package com.shuma.product.utils;


import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.shuma.product.utils.RedisConstants.*;


@Component
@Slf4j
public class CacheClient {
    //redis封装工具类
    @Resource
    private  StringRedisTemplate stringRedisTemplate;
    @Resource
    private RedissonClient redissonClient;

    public CacheClient(StringRedisTemplate stringRedisTemplate, RedissonClient redissonClient) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.redissonClient = redissonClient;
    }

    //数据写入redis 参数：key值，类型，基本时间，随机增加的最小时间，随机增加的最大时间，时间类型
    public void  set(String key, Object value, Long time, Long mintime,Long maxtime,TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),getCHACHE_TTL(time,mintime,maxtime),unit);
    }

    //解决redis查询缓存击穿（加锁），缓存穿透（redis设置空值），缓存雪崩（加时间随机）。
    //缓解缓存穿透，击穿和雪崩 参数：缓存key，锁key，id，类型，查找函数，缓存时间，随机增加缓存时间的最大和最小值，时间类型
    //有数据会按着数据类型返回，没有会返回空
    public <Ri,ID> Ri queryWithPassThrough(String keyPrefix,
                                           String lockPrefix,
                                           ID id,
                                           Class<Ri> type,
                                           Function<ID,Ri> dbFallback,
                                           Long time, Long mintime,
                                           Long maxtime,
                                           TimeUnit unit) throws InterruptedException {//R泛型
        String key = keyPrefix+ id;
        //1 从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        //2 存在，直接返回
        if(StrUtil.isNotBlank(json)) {//这个函数会把 “”空字符串 认定为空
            return JSONUtil.toBean(json, type);
        }
        //3 判断命中的是否为空值
        if( json != null){//""存在空字符串说明数据库中没有数据
            return null;
        }
        //4 实现缓存重建
        //4.1 获取互斥锁 解决缓存的击穿
        String lockKey = lockPrefix + id;
        //设置锁
        RLock lock = redissonClient.getLock(lockKey);
        //存放要查询的数据
        Ri ri = null;
        try {
            //尝试获取锁
            boolean isLock = lock.tryLock(LOCK_WAITTIME,LOCK_BASEGOODS_TTL,TimeUnit.SECONDS);
            //4. 2判断是否成功
            if(!isLock) {
                    //超过了重试次数，说明有问题返回空。
                    return null;
            }
            //重查，防止有之前的线程完成的相同操作
            json = stringRedisTemplate.opsForValue().get(key);
            if(StrUtil.isNotBlank(json)) {
                return JSONUtil.toBean(json, type);
            }
            if( json != null){
                return null;
            }
            //4.4成功，根据id查询数据库
            ri = dbFallback.apply(id);
            //5 不存在返回错误
            if(ri == null){
                // 将空值写入redis 解决缓存穿透
                stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
                //返回空值
                return null;
            }
            //6 存在，写入reids
            set(key,ri,time,mintime,maxtime,unit);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            //7 释放互斥锁
            lock.unlock();
        }
        return ri;
    }
    //通过设置增加随机时间来缓解 缓存雪崩
    //参数 基本时间，随机增加的最小时间，随机增加的最大时间
    private Long getCHACHE_TTL(Long time,Long Mintime,Long Maxtime){
        ThreadLocalRandom random = ThreadLocalRandom.current();
        return time + random.nextLong(Mintime, Maxtime + 1);
    }

}
